package com.magenta.maxoptra.gis.service;

import javax.management.*;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * Project: Santa-cruz
 * Author:  Irina Isaeva
 * Created: 14.06.12 14:17
 * <p/>
 * Copyright (c) 1999-2012 Magenta Corporation Ltd. All Rights Reserved.
 * Magenta Technology proprietary and confidential.
 * Use is subject to license terms.
 * <p/>
 * $Id$
 */
public class TypeSafeMBeanProxy {

    /**
     * There are no instances of this class.
     */
    private TypeSafeMBeanProxy() {
    }

    /**
     * Create an MBean proxy, checking that the target MBean exists
     * and implements the attributes and operations defined by the
     * given interface.
     *
     * @param mbsc      the MBean Server in which the proxied MBean is registered.
     * @param name      the ObjectName under which the proxied MBean is registered.
     * @param intfClass the MBean interface that the proxy will
     *                  implement by forwarding its methods to the proxied MBean.
     * @return The newly-created proxy.
     * @throws java.io.IOException                if there is a communication problem when
     *                                    connecting to the {@code MBeanServerConnection}.
     * @throws javax.management.InstanceNotFoundException  if there is no MBean
     *                                    registered under the given {@code name}.
     * @throws javax.management.NotCompliantMBeanException if {@code intfClass} is
     *                                    not a valid MBean interface.
     * @throws NoSuchMethodException      if a method in
     *                                    {@code intfClass} does not correspond to an attribute or
     *                                    operation in the proxied MBean.
     */
    public static <T> T newMBeanProxy(
            MBeanServerConnection mbsc,
            ObjectName name,
            Class<T> intfClass)
            throws IOException, InstanceNotFoundException,
            NotCompliantMBeanException, NoSuchMethodException {

        // Get the MBeanInfo, or throw InstanceNotFoundException
        final MBeanInfo mbeanInfo;
        try {
            mbeanInfo = mbsc.getMBeanInfo(name);
        } catch (InstanceNotFoundException e) {
            throw e;
        } catch (JMException e) {
            // IntrospectionException or ReflectionException:
            // very improbable in practice so just pretend the MBean wasn't there
            // but keep the real exception in the exception chain
            final String msg = "Exception getting MBeanInfo for " + name;
            InstanceNotFoundException infe = new InstanceNotFoundException(msg);
            infe.initCause(e);
            throw infe;
        }

        // Construct the MBeanInfo that we would expect from a Standard MBean
        // implementing intfClass.  We need a non-null implementation of intfClass
        // so we create a proxy that will never be invoked.
        final T impl = intfClass.cast(Proxy.newProxyInstance(
                intfClass.getClassLoader(), new Class<?>[]{intfClass}, nullIH));
        final StandardMBean mbean = new StandardMBean(impl, intfClass);
        final MBeanInfo proxyInfo = mbean.getMBeanInfo();

        checkMBeanInfos(intfClass.getClassLoader(), proxyInfo, mbeanInfo);
        return intfClass.cast(JMX.newMBeanProxy(mbsc, name, intfClass, false));
    }

    private static class NullInvocationHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) {
            return null;
        }
    }

    private static final NullInvocationHandler nullIH = new NullInvocationHandler();

    private static void checkMBeanInfos(
            ClassLoader loader, MBeanInfo proxyInfo, MBeanInfo mbeanInfo)
            throws NoSuchMethodException {

        // Check that every attribute accessible through the proxy is present
        // in the MBean.
        MBeanAttributeInfo[] mais = mbeanInfo.getAttributes();
        attrcheck:
        for (MBeanAttributeInfo pai : proxyInfo.getAttributes()) {
            for (MBeanAttributeInfo mai : mais) {
                if (compatibleAttributes(loader, pai, mai)) {
                    continue attrcheck;
                }
            }
            final String msg =
                    "Accessing attribute " + pai.getName() + " would fail";
            throw new NoSuchMethodException(msg);
        }

        // Check that every operation accessible through the proxy is present
        // in the MBean.
        MBeanOperationInfo[] mois = mbeanInfo.getOperations();
        opcheck:
        for (MBeanOperationInfo poi : proxyInfo.getOperations()) {
            for (MBeanOperationInfo moi : mois) {
                if (compatibleOperations(loader, poi, moi)) {
                    continue opcheck;
                }
            }
            final String msg =
                    "Accessing operation " + poi.getName() + " would fail";
            throw new NoSuchMethodException(msg);
        }
    }

    private static boolean compatibleAttributes(
            ClassLoader loader,
            MBeanAttributeInfo proxyAttrInfo, MBeanAttributeInfo mbeanAttrInfo) {
        if (!proxyAttrInfo.getName().equals(mbeanAttrInfo.getName())) {
            return false;
        }
        if (!proxyAttrInfo.getType().equals(mbeanAttrInfo.getType())) {
            if (proxyAttrInfo.isWritable()) {
                return false; // type must be identical
            }
            if (!isAssignable(loader,
                    proxyAttrInfo.getType(), mbeanAttrInfo.getType())) {
                return false;
            }
        }
        if (proxyAttrInfo.isReadable() && !mbeanAttrInfo.isReadable()) {
            return false;
        }
        if (proxyAttrInfo.isWritable() && !mbeanAttrInfo.isWritable()) {
            return false;
        }
        return true;
    }

    private static boolean compatibleOperations(
            ClassLoader loader,
            MBeanOperationInfo proxyOpInfo, MBeanOperationInfo mbeanOpInfo) {
        if (Arrays.asList(new String[]{"start", "stop", "create", "destroy"}).contains(proxyOpInfo.getName())) {
            return true;
        }
        if (!proxyOpInfo.getName().equals(mbeanOpInfo.getName()) ||
                !isAssignable(loader,
                        proxyOpInfo.getReturnType(),
                        mbeanOpInfo.getReturnType())) {
            return false;
        }
        MBeanParameterInfo[] proxyParams = proxyOpInfo.getSignature();
        MBeanParameterInfo[] mbeanParams = mbeanOpInfo.getSignature();
        if (proxyParams.length != mbeanParams.length) {
            return false;
        }
        for (int i = 0; i < proxyParams.length; i++) {
            if (!proxyParams[i].getType().equals(mbeanParams[i].getType())) {
                return false;
            }
        }
        return true;
    }

    private static boolean isAssignable(
            ClassLoader loader, String toClassName, String fromClassName) {
        if (toClassName.equals(fromClassName)) {
            return true;
        }
        try {
            Class<?> toClass = Class.forName(toClassName, false, loader);
            Class<?> fromClass = Class.forName(fromClassName, false, loader);
            return toClass.isAssignableFrom(fromClass);
        } catch (ClassNotFoundException e) {
            // Could not load one of the two classes so consider not assignable
            // In real code we might like to logger the exception
            return false;
        }
    }

}
